delphifandomcom-20200223-history
Creating DLLs
Introduction Delphi allows not only to create applications but also DLL files. A DLL file (short for dynamic link library) is a special kind of executable that cannot be started by itself, but exports procedures and functions (called "Entry Points") for other executables to call. Creating a DLL file with Delphi Creating a DLL file with Delphi consists of two steps: 1. Creating a library project 2. Exporting some functions from that library Creating a library project Delphi has got a special kind of project for a DLL file. To create one you use the File -> New -> Other menu entry to open a dialog where you can select the project type you want to create. There is one called "DLL Wizard"; that's the one we need here. Select it and press OK and Delphi will automatically create a new DLL file project for you. The source code looks like this: library Project1; uses SysUtils, Classes; {$R *.res} begin end. There is also a comment about using strings and ShareMem, but we can ignore that for now. At first glance, this looks like the project source for a normal console program. There is only one difference: It starts with the keyword library rather than program. Another difference is how it is listed in the project explorer. Where an application has got the extension ".exe" a library has got ".dll". So the new project is called "Project1.dll" rather than the usual "Project1.exe". Go ahead and save the new project giving it a name like "MyFirstLibrary". Exporting functions from a library Now let's export some function from our new library. Let's start with something very simple that does not involve complex data structures or a GUI: A function for adding two integers and returning their sum: function AddIntegers(const _a, _b: integer): integer; begin Result := _a + _b; end; Yes, that's not very exciting, is it? But it is a function that can be called from any Windows program that wants to do it. Side note about calling conventions Now, this is getting rather technical and confusing: Programming languages have evolved over the years to pass parameters and return values from and to function calls in different ways. Originally that was only inside one language and using only one compiler so the programmer just wrote the code and the compiler took care of how the functions were called. Then came DLL files and all of a sudden code written in one language using one compiler could be called from other code written in a different language or only using a different compiler. It turned out that this didn't work, so the compiler makers came up with the notion of calling conventions. We will use one called StdCall here since it allows for most other languages and compilers to call our DLL file. (One particular example is Visual Basic (from before .NET), which supports only the StdCall calling convention.) Exporting the function So let's add a calling convention to our function: function AddIntegers(const _a, _b: integer): integer; stdcall; begin Result := _a + _b; end; And export it: exports AddIntegers; Of course in a more complex DLL file we would not add all the code to the project source but to units, but we want to keep things simple here. So this is what our source code looks for now: library MyFirstLibrary; uses SysUtils, Classes; {$R *.res} function AddIntegers(const _a, _b: integer): integer; stdcall; begin Result := _a + _b; end; exports AddIntegers; begin end. Building the DLL file To create a 'DLL file '''from this library, sourcecode is as simple as creating an executable for a normal Delphi application: Just build it. You end up with a file called MyFirstLibrary.dll. Looking inside the DLL file Now, a DLL file is a binary file and if you e.g. load it into an editor, you will see lots of gibberish. But there are tools that can extract some more information about it, like Dependency Walker or the ''PE-Information Wizard in GExperts. If you open your DLL file with this wizard, it will tell you the following: (And lots of other things that we don't currently care about.) So it worked, you now have got a DLL file that exports the function AddIntegers. Using the DLL file So, now how do we call that DLL file? Simple: You just tell the compiler what DLL file to load and which function to call with which parameters. Creating a test project Close the library project (File -> Close All) and create a new console application (File -> New -> Other + Select "Console Application"). You will end up with an empty console application project like this: program Project2; {$APPTYPE CONSOLE} uses SysUtils; begin { TODO -oUser -cConsole Main : Insert code here } end. Save it as "MyLibraryTest" Calling the DLL function Add the following function declaration: function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; And call that function from within the main program: WriteLn(AddIntegers(1, 2)); Write('Press Enter'); ReadLn; Now your project should look like this: program MyLibraryTest; {$APPTYPE CONSOLE} uses SysUtils; function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; begin WriteLn(AddIntegers(1, 2)); Write('Press Enter'); ReadLn; end. Build your project and run it. You should now see a console window like this: Isn't that exciting? You just wrote your first DLL file and called it from a test program! ;-) Notes about memory management Now, do you remember that longish comment that Delphi automatically added to the library project? It said something about memory management and strings and ShareMem and stuff. Usually you don't care much about memory management in your applications. That's what the compiler and runtime libraries are for, right? But in the case of DLL files you are interfacing potentially with other compilers and runtime libraries, so you have to think about it. For example, Delphi automatically allocates and frees the memory for storing your strings, it knows when they are no longer needed, etc. The same applies for e.g. Visual Basic, but both do it in different ways. So, if you were to pass a string that was allocated by Visual Basic to a DLL file written in Delphi you would end up in big trouble, because both would now try to manage the string and would get into each other's hair. Passing strings So, in order to allow strings to be passed between an application and a DLL you must take extra care. Actually, the easiest way is just to not allow it at all! Now I can hear you thinking: "WTF is this guy talking about? Of course I want to pass strings from my application to the DLL file!" Yes, you would want to, wouldn't you? But you usually don't want to pass strings, but you want to pass the series of characters that are stored in a string. To do that you use something that comes from the ancient times when people where using primitive programming languages like C that didn't know such sophisticated data structures as strings (yes, I know, you C fanatics. C can do everything given the right libraries. This is meant to be a joke.). Back then programmers were stuck with something called zero-terminated strings or rather "a pointer to a array of characters which ends with the special character NUL". In Delphi that's called a PChar. Exporting a string function Now let's load our library project again and add a new function to it: function CountChars(_s: Pchar): integer; StdCall; begin Result := Length(_s); end; Notice something? No? Look closer? Still nothing? OK, I'll tell you: You know what the Length function does, don't you? It returns the number of characters stored in a string. But what do we pass to it? What is the parameter _s declared type? It's PChar, not String! Welcome to the conveniences of Delphi: It automatically converts a PChar to a string when assigning it to a string or passing it to a function expecting a string parameter. I hope you also noticed that we declared our new function as StdCall again. You should never forget to explicitly add a calling convention. If you run into access violations when calling a DLL function, that's the first thing to check: Do your program and your DLL file both use the same calling conventions? If not, you found the problem. We export the new function just the same way we exported the other one. You end up with a project source like this: library MyFirstLibrary; uses SysUtils, Classes; {$R *.res} function AddIntegers(_a, _b: integer): integer; stdcall; begin Result := _a + _b; end; function CountChars(_s: Pchar): integer; StdCall; begin Result := Length(_s); end; exports AddIntegers, CountChars; begin end. Save your changes and compile the project. Calling a string function Now we want to call that new function. So close the library project and open your test project again. Add the following function declaration: function CountChars(_s: Pchar): integer; StdCall; external 'MyFirstLibrary.dll'; and the following code to the main program: WriteLn(CountChars('hello')); Now your program should look like this: program MyLibraryTest; {$APPTYPE CONSOLE} uses SysUtils; function AddIntegers(_a, _b: integer): integer; stdcall; external 'MyFirstLibrary.dll'; function CountChars(_s: Pchar): integer; StdCall; external 'MyFirstLibrary.dll'; begin WriteLn(AddIntegers(1, 2)); WriteLn(CountChars('hello')); Write('Press Enter'); ReadLn; end. Again, notice something? Not? Look closer! What type of parameter do we pass to CountChars? Yes, that's a string constant! Welcome to the conveniences of Delphi again: Delphi automatically converts string constants to PChars if you pass them to a function that expect PChars. Compile and run your program. You should now get another output line saying "5", which incidentally is the number of characters in the string 'hello'. Now, was that painfull? You didn't technically pass a string, but for all practical purposes you did. Now let's try to pass an actual string, not a string constant. Declare a vaiable s and assign your name to it: var s: string; begin s := 'your name'; WriteLn('"', s, '" contains ', CountChars(s)); Write('Press Enter'); ReadLn; end. Compile and run it ... Oh? WTF? It doesn't compile. Welcome to the inconveniences of using PChars! Delphi does not automatically convert strings to PChars (don't ask me, why). You have to explicitly tell it to do that by typecasting. So change your code to: WriteLn('"', s, '" contains ', CountChars(PChar(s))); Compile and run it. You should now get an output like this: "your name" contains 9 characters Press Enter Other pitfalls In Delphi you are probably used to returning all sorts of data types as function results, e.g. strings or classes. When using DLL files, you should not do that. Only ever return simple data types like integer, double or single, otherwise you will most likely end up with a DLL file that you can only use from a Delphi application (and even that will be a major pain in the lower back to get to work). Again, this is a matter of memory management and different compilers / runtime libraries. Array and Record alignments are another annoyance when passing parameters over DLL file boundaries. For performance reasons modern compilers arrange all data types to word- double-word- or even quad-word addresses. But if you don't know what the compilers on both sides actually use, you may get very peculiar effects. Delphi allows you to declare arrays and records as packed, meaning that no alignment takes place, but other programming languages or compilers may not have that concept. Delphi also allows you to force some alignment using the $A or $Align compiler directive, again, other programming languages or compilers may not give you that option. It's sometimes up to trial and error to find out how a structure is being passed to your program. Your best bet is usually to use data types on both sides that are multiples of 4 bytes long. ShareMem Just going back to the longish comment that Delphi automatically places into a new DLL file project. If your DLL file and your main program will both be Delphi programs (but are you sure that will always be the case?), you can take that advice and include the unit ShareMem as the first unit in the uses clause of both, the program and the DLL file. If you do that you can happily pass strings and other automatically memory managed data types between the program and the DLL file. But never ever should you pass objects! The reason is that even though you might use the same class declaration in both, the program and the DLL file, these objects are not the the same and they might divert from each other as your code evolves. A recommendation for advanced users is to use a third-party memory manager called FastMM. It's good enough that Borland actually use it as the primary memory manager in Delphi 2006 and onwards. If you're using a version before that, then you can download it for free and use it on a project by project basis. A big benefit for DLL files is that when you're using FastMM, you can use standard String variables in your exported procedures and functions without needing to worry about ShareMem, or distributing anything else with your project or DLL files. Conclusion Writing DLL files in Delphi is simple and also powerful. If you are going to call these DLL files only from other Delphi applications, you might want to look into packages first. If you want to call Delphi code from other programming languages, you may want to look into COM servers (but be warned: While using COM servers from e.g. Visual Basic is very simple, it can be complex from other languages and can be a pain to have to register them every time before being able to call them). Category:Examples Category:Source Code Category:DLL